package org.dromara.common.tenant.helper;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;

import java.util.function.Supplier;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：租户助手
 */
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TenantHelper {

    /**
     * 定义一个静态常量，用于存储动态租户的Redis键名
     */
    private static final String DYNAMIC_TENANT_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "dynamicTenant";

    /**
     * 用ThreadLocal创建一个临时的动态租户存储空间，确保每个线程内独立
     */
    private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new TransmittableThreadLocal<>();

    /**
     * 租户功能是否启用
     * 检查租户功能是否启用
     *
     * 通过查询Spring配置文件中"tenant.enable"属性的值来确定租户功能是否启用
     * 如果该属性未设置，则默认返回false，表示租户功能未启用
     *
     * @return true 如果租户功能已启用；否则返回false
     */
    public static boolean isEnable() {
        // 从Spring配置文件中获取"tenant.enable"属性的值，并将其转换为布尔值
        // 如果属性未设置或值为false，则返回false，表示租户功能未启用
        return Convert.toBool(SpringUtils.getProperty("tenant.enable"), false);
    }

    /**
     * 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
     * 启用忽略拦截器配置
     * 本方法主要用于配置全局的拦截器忽略策略，通过构建一个忽略策略对象，设置租户线路为true，然后调用InterceptorIgnoreHelper的handle方法来应用该策略
     * 通过这种方式，可以在程序中灵活地控制哪些情况下忽略拦截器的行为
     */
    public static void enableIgnore() {
        // 构建一个忽略策略对象，设置租户线路为true 应用忽略策略
        InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
    }

    /**
     * 关闭忽略租户
     * 该方法通过调用 InterceptorIgnoreHelper 的 clearIgnoreStrategy 方法来实现禁用当前的忽略策略
     */
    public static void disableIgnore() {
        InterceptorIgnoreHelper.clearIgnoreStrategy();
    }

    /**
     * 在忽略租户中执行
     * 忽略执行过程中的特定行为或事件
     * 此方法通过在执行前后分别启用和禁用忽略功能，来确保在执行过程中不会受到特定行为或事件的影响
     *
     * @param handle 处理执行方法 待执行的Runnable任务此任务在被调用执行时，将会处于忽略状态
     */
    public static void ignore(Runnable handle) {
        // 启用忽略状态，屏蔽特定行为或事件
        enableIgnore();
        try {
            // 执行传入的Runnable任务
            handle.run();
        } finally {
            // 禁用忽略状态，恢复对特定行为或事件的正常响应
            disableIgnore();
        }
    }

    /**
     * 在忽略租户中执行
     * 忽略特定操作的工具方法
     * 此方法在执行供应商提供的操作前启用忽略机制，并在操作完成后（无论是否成功）禁用忽略机制
     * 主要用于处理那些可能产生不影响程序继续执行的特定异常或情况
     *
     * @param handle 处理执行方法 供应商函数，提供在忽略机制启用时要执行的操作
     * @param <T> 操作返回值的类型
     * @return 操作的执行结果
     */
    public static <T> T ignore(Supplier<T> handle) {
        // 启用忽略机制
        enableIgnore();
        try {
            // 执行供应商提供的操作
            return handle.get();
        } finally {
            // 确保在finally块中禁用忽略机制，以恢复正常状态
            disableIgnore();
        }
    }

    /**
     * 设置动态租户ID
     * 此方法主要用于配置租户ID，以支持多租户环境下的动态数据源切换
     * 通常用于在多租户应用程序中根据不同的租户ID加载相应的数据配置
     *
     * @param tenantId 租户ID，用于标识不同的租户
     */
    public static void setDynamic(String tenantId) {
        setDynamic(tenantId, false);
    }

    /**
     * 设置动态租户(一直有效 需要手动清理)
     * <p>
     * 如果为未登录状态下 那么只在当前线程内生效
     * 此方法用于根据租户ID和全局标志动态设置租户信息
     * 它主要服务于多租户应用场景，允许在全局或用户级别动态更改租户设置
     *
     * @param tenantId 租户id 租户标识符如果指定了全局标志，这将代表全局租户ID；否则，它将代表当前用户的租户ID
     * @param global   是否全局生效 指示是否为全局租户设置如果为true，则tenantId将应用于所有用户；如果为false，则tenantId将仅应用于当前用户
     */
    public static void setDynamic(String tenantId, boolean global) {
        // 检查功能是否启用，如果未启用则直接返回
        if (!isEnable()) {
            return;
        }
        // 如果用户未登录或全局标志为false，将租户ID设置到线程局部变量中，以便在请求期间使用
        if (!isLogin() || !global) {
            TEMP_DYNAMIC_TENANT.set(tenantId);
            return;
        }
        // 生成缓存键，用于在Redis中存储当前用户的动态租户ID
        String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
        // 将租户ID存储到Redis中，以便在后续请求中识别用户的租户设置
        RedisUtils.setCacheObject(cacheKey, tenantId);
    }

    /**
     * 获取动态租户(一直有效 需要手动清理)
     * 此方法用于根据当前会话的状态动态获取租户标识它首先检查是否启用了动态租户功能，
     * 如果没有启用，则返回null如果当前会话已登录，但未设置动态租户标识，则从Redis缓存中
     * 获取对应的租户标识
     *
     * <p>
     * 如果为未登录状态下 那么只在当前线程内生效
     * @return 当前会话的租户标识，如果未启用动态租户或当前会话未登录且未设置租户标识，则返回null
     */
    public static String getDynamic() {
        // 检查是否启用了动态租户功能，如果没有启用，则返回null
        if (!isEnable()) {
            return null;
        }
        // 检查当前是否已登录，如果没有登录，则返回临时租户标识
        if (!isLogin()) {
            return TEMP_DYNAMIC_TENANT.get();
        }
        // 如果线程内有值 优先返回
        String tenantId = TEMP_DYNAMIC_TENANT.get();
        if (StringUtils.isNotBlank(tenantId)) {
            return tenantId;
        }
        // 构建Redis缓存的键，用于获取当前用户的租户标识
        String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
        // 从Redis缓存中获取租户标识
        tenantId = RedisUtils.getCacheObject(cacheKey);
        return tenantId;
    }

    /**
     * 清除动态租户
     * 此方法首先检查是否启用租户功能，如果未启用则直接返回
     * 如果未登录，仅清除临时的动态租户信息
     * 如果已登录，除了清除临时的动态租户信息外，还会从缓存中删除与用户ID关联的动态租户信息
     */
    public static void clearDynamic() {
        // 检查是否启用租户功能，未启用则返回
        if (!isEnable()) {
            return;
        }
        // 检查用户是否登录，未登录仅清除临时租户信息
        if (!isLogin()) {
            TEMP_DYNAMIC_TENANT.remove();
            return;
        }
        // 已登录情况下，清除临时租户信息，并从Redis缓存中删除与用户ID关联的动态租户信息
        TEMP_DYNAMIC_TENANT.remove();
        String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
        RedisUtils.deleteObject(cacheKey);
    }

    /**
     * 在动态租户中执行
     * 动态设置租户信息，并在执行完指定的操作后清除该信息
     * 此方法提供了一种在操作过程中临时设置和清除租户信息的方式，以支持跨租户操作
     *
     * @param tenantId 租户ID，用于标识不同的租户
     * @param handle 处理执行方法 一个Runnable对象，封装了需要在特定租户环境下执行的操作
     */
    public static void dynamic(String tenantId, Runnable handle) {
        // 设置动态租户信息
        setDynamic(tenantId);
        try {
            // 执行指定的操作
            handle.run();
        } finally {
            // 清除动态租户信息，确保线程的干净和隔离
            clearDynamic();
        }
    }

    /**
     * 在动态租户中执行
     * 动态设置租户信息，并在供应商操作完成后清除动态设置的租户信息
     * 此方法用于在执行特定操作时临时设置租户ID，以确保操作可以在正确的租户上下文中执行，
     * 并在操作完成后避免潜在的内存泄漏
     *
     * @param tenantId 租户ID，用于临时设置租户信息
     * @param handle 处理执行方法 一个供应商接口，包含需要在特定租户上下文中执行的操作
     * @param <T>      供应商返回值的类型
     * @return 供应商执行结果
     */
    public static <T> T dynamic(String tenantId, Supplier<T> handle) {
        setDynamic(tenantId);
        try {
            return handle.get();
        } finally {
            clearDynamic();
        }
    }

    /**
     * 获取当前租户id(动态租户优先)
     * 此方法首先检查系统是否启用租户支持，如果未启用，则返回null
     * 如果已启用，它会尝试从动态数据源获取租户ID，如果未获取到，则尝试从登录信息中获取
     *
     * @return 当前租户ID，如果未启用租户支持或无法获取租户ID，则返回null
     */
    public static String getTenantId() {
        // 检查是否启用租户支持
        if (!isEnable()) {
            // 如果未启用，则返回null
            return null;
        }
        // 尝试从动态数据源获取租户ID
        String tenantId = TenantHelper.getDynamic();
        // 如果从动态数据源获取的租户ID为空或空白，则尝试从登录信息中获取
        if (StringUtils.isBlank(tenantId)) {
            tenantId = LoginHelper.getTenantId();
        }
        // 返回获取到的租户ID
        return tenantId;
    }

    /**
     * 检查用户是否已登录
     *
     * @return 如果用户已登录，则返回true；否则返回false
     *
     * 此方法通过调用StpUtil的checkLogin方法来验证用户是否已登录
     * 如果checkLogin方法抛出异常，则意味着用户未登录
     */
    private static boolean isLogin() {
        try {
            StpUtil.checkLogin();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

}
